#include "stdafx.h"
#include "resource.h"
#include "FindTextDialog.h"
#include <assert.h>
#include <iterator>

extern char32_t ToggleCase(char32_t inChar);

FindTextDialog::FindTextDialog()
{
	mFindDialogWindow = 0;
	mFindDialogMessage = ::RegisterWindowMessage(FINDMSGSTRING);
	ZeroMemory(&mFindReplaceStruct, sizeof(mFindReplaceStruct));
	mFindReplaceStruct.lStructSize = sizeof(mFindReplaceStruct);
	mFindWhat[0] = '\0';
}


FindTextDialog::~FindTextDialog()
{
	CloseDialog();
}

void FindTextDialog::SetInstance(HINSTANCE inInstance)
{
	mFindReplaceStruct.hInstance = inInstance;
}
void FindTextDialog::SetParentWindow(HWND inWindow)
{
	mFindReplaceStruct.hwndOwner = inWindow;
}

UINT_PTR CALLBACK FRHookProc(
	HWND hdlg, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
	if (WM_INITDIALOG == uiMsg)
	{
		auto pThis = reinterpret_cast<FINDREPLACE*>(lParam)->lCustData;
		SetWindowLongPtr(hdlg, GWLP_USERDATA, pThis);
		return true;
	}
	if (WM_CTLCOLORSTATIC == uiMsg)
	{
		const HWND statusLabel = ::GetDlgItem(hdlg, IDC_STATUSSTRINGFORFIND);
		if (statusLabel == HWND(lParam))
		{
			COLORREF textColor = ::GetSysColor(COLOR_GRAYTEXT);
			::SetTextColor(HDC(wParam), textColor);
			return reinterpret_cast<INT_PTR>(::GetSysColorBrush(COLOR_3DFACE));
		}
	}
	if (WM_COMMAND == uiMsg)
	{
		bool isManualChange = false;
		const WORD controlId = LOWORD(wParam);
		if (controlId == chx2)
		{
			if (auto p = reinterpret_cast<FindTextDialog*>(
				::GetWindowLongPtr(hdlg, GWLP_USERDATA)))
			{
				p->OnStringChange();
			}
			isManualChange = true;
		}
		else if (controlId == edt1 && HIWORD(wParam) == EN_CHANGE)
		{
			HWND labelWithStatus = ::GetDlgItem(hdlg, IDC_STATUSSTRINGFORFIND);
			::ShowWindow(labelWithStatus, SW_HIDE);

			if (auto p = reinterpret_cast<FindTextDialog*>(
				::GetWindowLongPtr(hdlg, GWLP_USERDATA)))
			{
				p->OnStringChange();
			}
			isManualChange = true;
		}
		else if (controlId == rad1 || controlId == rad2)
		{
			isManualChange = true;
		}
		if (isManualChange)
		{
			if (HWND parent = ::GetParent(hdlg))
			{
				::PostMessage(parent, kWmMessageFindSettingsAreChanged, 0, 0);
			}
		}
	}
	return false;
}

void FindTextDialog::ReopenDialog(SearchKind inSearchKind)
{
	if (mFindDialogWindow)
	{
		::DestroyWindow(mFindDialogWindow);
		mFindDialogWindow = 0;
	}
	mSearchKind = inSearchKind;

	mFindReplaceStruct.Flags = FR_HIDEWHOLEWORD | FR_ENABLEHOOK | FR_DOWN | FR_ENABLETEMPLATE;
	mFindReplaceStruct.lpstrFindWhat = mFindWhat;
	mFindReplaceStruct.wFindWhatLen = kFindWhatLength;
	mFindReplaceStruct.lCustData = LPARAM(this);
	mFindReplaceStruct.lpfnHook = FRHookProc;
	mFindReplaceStruct.lpTemplateName = MAKEINTRESOURCE(IDD_DIALOG_FINDUNICODE);

	if (SearchKind::kBytesFromHexDigits == mSearchKind)
	{
		mFindReplaceStruct.Flags |= FR_HIDEMATCHCASE;
		mFindReplaceStruct.lpTemplateName = MAKEINTRESOURCE(IDD_DIALOG_FINDHEXDIGITS);
	}

	mFindDialogWindow = ::FindText(&mFindReplaceStruct);

	auto extraDescription = GetExtraDescription();
	if (0 != mFindDialogWindow && nullptr != extraDescription)
	{
		TCHAR buf[100];
		int length = ::GetWindowText(mFindDialogWindow, buf, 100);
		if (0 < length)
		{
			std::basic_string<TCHAR> title(buf);
			title += L" - ";
			title += extraDescription;
			::SetWindowText(mFindDialogWindow, title.c_str());
		}
	}

	HWND labelWithStatus = ::GetDlgItem(mFindDialogWindow, IDC_STATUSSTRINGFORFIND);
	::ShowWindow(labelWithStatus, SW_HIDE);

	if ('\0' != mFindWhat[0] && 0 != mFindDialogWindow)
	{
		OnStringChange();
	}
}

void FindTextDialog::CloseDialog()
{
	if (mFindDialogWindow)
	{
		::DestroyWindow(mFindDialogWindow);
		mFindDialogWindow = 0;
	}
}

int FindTextDialog::GetWmMessageId() const
{
	return mFindDialogMessage;
}

void FindTextDialog::NullifyDialogHandle()
{
	mFindDialogWindow = nullptr;
}

bool FindTextDialog::IsProcessedDlgMessage(MSG* msg)
{
	return (nullptr != mFindDialogWindow && 
		::IsDialogMessage(mFindDialogWindow, msg));
}

void FindTextDialog::DisplayStatus(LPCTSTR inString)
{
	if (mFindDialogWindow)
	{
		if (HWND label = ::GetDlgItem(mFindDialogWindow, IDC_STATUSSTRINGFORFIND))
		{
			if (::SetWindowText(label, inString))
			{
				::ShowWindow(label, SW_SHOW);
			}
		}
	}
}

void FindTextDialog::EnableTheFindNextButton(bool inEnable)
{
	if (mFindDialogWindow)
	{
		bool enable = inEnable;
		if (enable)
		{
			// calculate whether the other controls permit "FindNext"
			if (SearchKind::kBytesFromHexDigits == mSearchKind)
			{
				enable = MakeBytesFromHexDigits();
			}
			else
			{
				TCHAR buf[2];
				UINT len = ::GetDlgItemText(mFindDialogWindow, edt1, buf, 2);
				enable = (0 < len);
			}
		}
		if (HWND button = ::GetDlgItem(mFindDialogWindow, IDOK))
		{
			::EnableWindow(button, enable);
		}
	}
}

const WhatCharactersVector& FindTextDialog::GetWhat(FINDREPLACE* inStruct)
{
	assert(&mFindReplaceStruct == inStruct);
	return mWhatVector;
}

bool FindTextDialog::IsSearchingDown() const
{
	return 0 != (mFindReplaceStruct.Flags & FR_DOWN);
}
// --- private -----

LPCTSTR FindTextDialog::GetExtraDescription() const
{
	switch (mSearchKind)
	{
	case SearchKind::kBytesFromHexDigits:	
		return L"via hex digits";
	case SearchKind::kUtf8:
		return L"UTF-8";
	case SearchKind::kUtf16LE:
		return L"UTF-16LE";
	case SearchKind::kUtf16BE:
		return L"UTF-16BE";
	}
	return nullptr;
}

static
VOID CALLBACK TimerProcDisableOkButton(HWND hwnd, UINT uMsg, UINT_PTR idEvent,
	DWORD dwTime)
{
	if (HWND hwndOk = ::GetDlgItem(hwnd, IDOK))
	{
		::EnableWindow(hwndOk, FALSE);
	}
	::KillTimer(hwnd, idEvent);
}

void FindTextDialog::OnStringChange()
{
	mCurrentFindWhatLength = ::GetDlgItemText(mFindDialogWindow, edt1,
		mFindWhat, kFindWhatLength);

	if (SearchKind::kBytesFromHexDigits == mSearchKind)
	{
		if (0 < mCurrentFindWhatLength && !MakeBytesFromHexDigits())
		{
			::SetTimer(mFindDialogWindow, 5, USER_TIMER_MINIMUM,
				::TimerProcDisableOkButton);
		}
		return;
	}
	UINT matchCaseState = ::IsDlgButtonChecked(mFindDialogWindow, chx2);
	bool isMatchingCase = (BST_UNCHECKED != matchCaseState);

	What32Vector v;
	auto&& text32 = GetWhatText32();
	if (isMatchingCase)
	{
		v.assign(1, text32);
	}
	else
	{
		SplitByCase(text32, v);
	}

	mWhatVector.clear();
	for (auto string : v)
	{
		mWhatVector.push_back(ByteVector());
		if (!string.empty())
		{
			ByteVector& bv = mWhatVector.back();
			for (char32_t ch : string)
			{
				OutputToBuffer(ch, bv);
			}
		}	
	}
	ByteVector bytesLow;
	CopyBytesTo(bytesLow, false);
	ShowBytes(bytesLow, IDC_HEXEXPANSION1);

	ByteVector bytesHigh;
	if (1 < mWhatVector.size())
	{
		CopyBytesTo(bytesHigh, true);
	}
	ShowBytes(bytesHigh, IDC_HEXEXPANSION2);
}

bool FindTextDialog::MakeBytesFromHexDigits()
{
	if (1 != mWhatVector.size())
	{
		mWhatVector.resize(1);
	}
	ByteVector& v = mWhatVector.front();
	v.clear();
	int nibbleValue = 0;
	bool isWaitingSecondDigit = false;
	for (auto p = mFindWhat; '\0' != *p && p < mFindWhat + kFindWhatLength; ++p)
	{
		if (*p < 127)
		{
			char ch = char(*p);
			if (::isxdigit(ch))
			{
				static const char kDigits[] = "0123456789abcdef";
				int i = ::strchr(kDigits, ::tolower(ch)) - kDigits;
				if (isWaitingSecondDigit)
				{
					v.push_back(BYTE((nibbleValue << 4) + i));
				}
				else
				{
					nibbleValue = i;
				}
				isWaitingSecondDigit = !isWaitingSecondDigit;
				continue;
			}
			if (::isspace(ch) && !isWaitingSecondDigit)
			{
				// allow spaces between pairs of hex digits
				continue;
			}
		}
		v.clear();
		break;
	}

	return !v.empty() && !isWaitingSecondDigit;
}

std::u32string FindTextDialog::GetWhatText32()
{
	assert(sizeof(mFindWhat[0]) == sizeof(char16_t));

	std::u32string result;
	char32_t highSurrogate = 0;

	bool isConversionError = false;
	for (LPCTSTR p = mFindWhat; *p && !isConversionError; ++p)
	{
		char16_t w = *p;
		if (highSurrogate)
		{
			if (0xdc00 <= w && w <= 0xdfff)
			{
				result.push_back((highSurrogate << 10) + (w & 0x3ff));
				highSurrogate = 0;
			}
			else
			{
				isConversionError = true;
			}
		}
		else if (w < 0xd800 || w >= 0xe000)
		{
			result.push_back(w);
		}
		else if (0xd800 <= w && w < 0xdc00)
		{
			highSurrogate = (w & 0x3ff) + 64;
		}
		else
		{
			isConversionError = true;
		}
	}
	assert(!isConversionError && 0 == highSurrogate);
	return result;
}

void FindTextDialog::SplitByCase(const std::u32string& inString, What32Vector& outVector)
{
	bool wasCaseIndifferent = false;
	for (char32_t ch : inString)
	{
		char32_t ch1 = ToggleCase(ch);
		if (ch1 == ch)
		{
			if (!wasCaseIndifferent)
			{
				outVector.push_back(std::u32string());
				wasCaseIndifferent = true;
			}
			outVector.back().push_back(ch);
			continue;
		}
		if (wasCaseIndifferent)
		{
			outVector.push_back(std::u32string());
			wasCaseIndifferent = false;
		}
		char32_t smaller = min(ch, ch1);
		char32_t bigger = max(ch, ch1);
		outVector.push_back(std::u32string(1, smaller));
		outVector.push_back(std::u32string(1, bigger));
	}
}

void FindTextDialog::OutputToBuffer(char32_t inChar, ByteVector& outBuffer)
{
	assert(inChar <= 0x10ffff);
	if (SearchKind::kUtf8 == mSearchKind)
	{
		if (inChar <= 0x7f)
		{
			outBuffer.push_back(BYTE(inChar));
		}
		else
		{
			const int insertionPos = outBuffer.size();
			char32_t firstByteLimit = 0x40;
			for (auto x = inChar; 0 != x; x >>= 6, firstByteLimit >>= 1)
			{
				auto b = x < firstByteLimit ? x - (2 * firstByteLimit) : 0x80 + (x & 0x3f);
				BYTE b1 = BYTE(b & 0xff);
				auto iter = outBuffer.begin() + insertionPos;
				outBuffer.insert(iter, b1);
			}
		}
		return;
	}
	assert(SearchKind::kUtf16LE == mSearchKind || SearchKind::kUtf16BE == mSearchKind);
	char16_t words[2];
	int n = 1;
	if (inChar <= 0xffff)
	{
		words[0] = char16_t(inChar);
	}
	else
	{
		words[0] = char16_t( (inChar >> 10) + 0xd7c0);
		words[1] = char16_t((inChar & 0x3ff) + 0xdc00);
		n = 2;
	}
	for (int i = 0; i < n; ++i)
	{
		char16_t w = words[i];
		if (SearchKind::kUtf16LE == mSearchKind)
		{
			outBuffer.push_back(LOBYTE(w));
			outBuffer.push_back(HIBYTE(w));
		}
		else
		{
			outBuffer.push_back(HIBYTE(w));
			outBuffer.push_back(LOBYTE(w));
		}
	}
}

void FindTextDialog::CopyBytesTo(ByteVector& outVector, bool inIsLow) const
{
	for (size_t i = inIsLow ? 0 : 1; i < mWhatVector.size(); i += 2)
	{
		size_t j = i;
		if (mWhatVector[i].empty() && !inIsLow)
		{
			--j;
		}
		auto& v = mWhatVector[j];
		std::copy(v.begin(), v.end(), back_inserter(outVector));
	}
	if (!inIsLow && 1 == (mWhatVector.size() & 1))
	{
		auto& v = mWhatVector.back();
		std::copy(v.begin(), v.end(), back_inserter(outVector));
	}
}

void  FindTextDialog::ShowBytes(const ByteVector& inVector, int inContolId)
{
	// no need to show all bytes, the shown string may be truncated
	static const int kBufferLen = 38;
	char text[kBufferLen];
	text[0] = '\0';
	int pos = 0;
	for (size_t i = 0; i < inVector.size(); ++i)
	{
		pos += ::sprintf_s(text + pos, kBufferLen - pos, "%.2X ", inVector[i]);
		if (pos + 4 >= kBufferLen)
		{
			::sprintf_s(text + kBufferLen - 4, 4, "...");
			break;
		}
	}
	::SetDlgItemTextA(mFindDialogWindow, inContolId, text);
}